package eztools

import (
	"archive/zip"
	"io"
	"io/fs"
	"mime"
	"os"
	"path/filepath"
)

const (
	// FileCreatePermission 0644
	FileCreatePermission = 0644
	// DirCreatePermission 0755
	DirCreatePermission = 0755
	// FileSizeMax2Split size of a file piece
	FileSizeMax2Split = 2<<31 - 1
)

// FileReadByPiece reads a file by hunk in a loop
// uint32 is to hold file size, so its max size is 4'294'967'295
// Parameters:
//
//	readSz() returns how much to read, every time
//	fun() handles the read part, every time, returning whether to stop
//
// Return values:
//
//	error from fun()
//	ErrIncomplete if the file is zero sized
//	ErrOutOfBound if the size of the file is too big
//	ErrInvalidInput if the size of the part read does not equal the request
//	from os.Stat(), os.OpenFile(), os.File.Seek(), os.File.Read()
func FileReadByPiece(file string, readSz func(indx uint32,
	inf fs.FileInfo, done uint32) uint32,
	fun func(indx uint32, inf fs.FileInfo, offset uint32,
		bytes []byte) error) (err error) {
	inf, err := os.Stat(file)
	if err != nil {
		return
	}
	if inf.Size() > FileSizeMax2Split {
		return ErrOutOfBound
	}
	sz := uint32(inf.Size())
	if sz == 0 {
		return ErrIncomplete
	}
	fp, err := os.OpenFile(file, os.O_RDONLY, FileCreatePermission)
	if err != nil {
		return
	}
	defer fp.Close()
	var offset, indx uint32
	for offset < sz {
		sz := readSz(indx, inf, offset)
		_, err = fp.Seek(int64(offset), 0)
		if err != nil {
			return
		}
		buf := make([]byte, sz)
		n, err := fp.Read(buf)
		if err != nil {
			return err
		}
		if uint32(n) != sz {
			return ErrInvalidInput
		}
		err = fun(indx, inf, offset, buf)
		if err != nil {
			return err
		}
		offset += uint32(sz)
		indx++
	}
	return
}

// FileReaderByPiece reads a file by hunk in a loop
// Parameters:
func FileReaderByPiece(rdr io.ReadCloser, name string, readSz func(indx uint32,
	name string) (uint32, string),
	fun func(indx uint32, name string,
		bytes []byte, done bool) error) (err error) {
	defer rdr.Close()
	var sz, indx uint32
	for {
		sz, name = readSz(indx, name)
		buf := make([]byte, sz)
		n, err := rdr.Read(buf)
		if err != nil {
			if err == io.EOF {
				return fun(indx, name, buf[:n], true)
			}
			return err
		}
		done := uint32(n) < sz
		err = fun(indx, name, buf[:n], done)
		if err != nil || done {
			return err
		}
		indx++
	}
}

// FileAppend appends bytes to file
//
//	file will be created, if not exists yet, but parent directories cannot be created.
func FileAppend(file string, bytes []byte) (err error) {
	f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, FileCreatePermission)
	if err != nil {
		return
	}
	defer f.Close()
	_, err = f.Write(bytes)
	return
}

// FileWrite writes bytes to file
// Return value: from os.WriteFile() or os.MkdirAll(), if failed to creat parent directories
func FileWrite(file string, bytes []byte) error {
	path := filepath.Dir(file)
	if _, err := os.Stat(path); err != nil {
		err = os.MkdirAll(path, DirCreatePermission)
		if err != nil {
			return err
		}
	}
	return os.WriteFile(file, bytes, FileCreatePermission)
}

// FileWriteNoCreate writes input bytes to existing file
// Return value: from FileWrite() or os.Stat()
func FileWriteNoCreate(file string, bytes []byte) error {
	if _, err := os.Stat(file); err != nil {
		return err
	}
	return FileWrite(file, bytes)
}

// FileDel file deletion
func FileDel(fn string) error {
	if err := os.Remove(fn); err != nil {
		return err
	}
	parent := filepath.Dir(fn)
	f, err := os.Open(parent)
	if err != nil {
		return nil
	}
	_, err = f.Readdirnames(1)
	if err == nil {
		return nil
	}
	os.Remove(parent)
	return nil
}

// FileType returns MIME type of a file by extension
func FileType(fn string) string {
	return mime.TypeByExtension(filepath.Ext(fn))
	// http.DetectContentType([]byte)
}

// ZipWtReaderWriter zips files iterated by getRd to wr
//
//	readers and wr are Close()'ed before returning
//	the last reader must be nil to stop iteration
//
//	Return value: error to create result file,
//		or, error to each file to be packed and to final closure
func ZipWtReaderWriter(getRd func() (string, io.ReadCloser), wr io.WriteCloser) (ret []error) {
	defer wr.Close()
	zpWr := zip.NewWriter(wr)

	for {
		fn, fp := getRd()
		if fp == nil {
			break
		}
		f, err := zpWr.Create(fn)
		if err == nil {
			_, err = io.Copy(f, fp)
		}
		ret = append(ret, err)
		// fp.Close()
	}

	// Make sure to check the error on Close.
	return append(ret, zpWr.Close())
}

// ZipWtWriter zips files to wr, with relative directory baseDir
//
//	Parameter: baseDir if "", no dir info packed
//	Return value: error to create result file,
//		or, error to each file to be packed and to final closure
func ZipWtWriter(files []string, baseDir string, wr io.WriteCloser) (ret []error) {
	defer wr.Close()
	zpWr := zip.NewWriter(wr)

	for _, file := range files {
		pa, err := func() (string, error) {
			if len(baseDir) > 0 {
				return filepath.Rel(baseDir, file)
			}
			return "", ErrIncomplete
		}()
		if err != nil {
			pa = filepath.Base(file)
		}
		f, err := zpWr.Create(pa)
		if err != nil {
			ret = append(ret, err)
			continue
		}
		fp, err := os.OpenFile(file, os.O_RDONLY, FileCreatePermission)
		if err == nil {
			_, err = io.Copy(f, fp)
		}
		ret = append(ret, err)
		fp.Close()
	}

	// Make sure to check the error on Close.
	return append(ret, zpWr.Close())
}

// Zip zips files to zpName, with relative directory baseDir
//
//	Parameter: baseDir if "", no dir info packed
//	Return value: error to create result file,
//		or, error to each file to be packed and to final closure
func Zip(files []string, baseDir, zpName string) (ret []error) {
	wr, err := os.OpenFile(zpName,
		os.O_TRUNC|os.O_CREATE|os.O_WRONLY, FileCreatePermission)
	if err != nil {
		return []error{err}
	}
	return ZipWtWriter(files, baseDir, wr)
}
